package cn.infinite.security.filter;

import cn.infinite.security.configuration.RedisKeySet;
import cn.infinite.security.configuration.SmsConfigurerProperties;
import cn.infinite.security.error.ImageCheckCodeMismatchException;
import cn.infinite.security.error.ImageCheckCodeRequiredException;
import cn.infinite.security.error.MobileNotFoundException;
import cn.infinite.security.error.SmsCodeNotFoundException;
import cn.infinite.security.token.SmsAuthenticationToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;

/**
 * @author zhangqi
 */
@Slf4j
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    protected SmsConfigurerProperties smsConfigurerProperties;

    protected RedisTemplate<String, String> redisTemplate;

    protected HashOperations<String, String, String> hashOperations;

    private String mobileParam;

    private String smsCodeParam;

    // ~ Constructors
    // ===================================================================================================
    private String imageCheckCodeParam;

    // ~ Methods
    // ========================================================================================================

    public SmsAuthenticationFilter(SmsConfigurerProperties smsConfigurerProperties, RedisTemplate redisTemplate) {
        super(new AntPathRequestMatcher(smsConfigurerProperties.getScopePath().concat(smsConfigurerProperties
                .getValidateSmsCodeEndPoint()), "POST"));

        assert StringUtils.isNotEmpty(smsConfigurerProperties.getMobileParamName());
        assert StringUtils.isNotEmpty(smsConfigurerProperties.getCheckCodeParamName());
        this.redisTemplate = redisTemplate;
        this.mobileParam = smsConfigurerProperties.getMobileParamName();
        this.smsCodeParam = smsConfigurerProperties.getCheckCodeParamName();
        this.imageCheckCodeParam = smsConfigurerProperties.getImageCheckCodeParamName();
        this.smsConfigurerProperties = smsConfigurerProperties;
        this.hashOperations = this.redisTemplate.opsForHash();
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String smsCode = obtainSmsCode(request);
        String username = obtainUserName(request);

        Integer maxFailCount = this.smsConfigurerProperties.getImageCheckCodeWhenMaxFailCount();

        if (maxFailCount >= 0 && Integer.valueOf(Optional.ofNullable(this.hashOperations.get(RedisKeySet
                .NEED_IMAGE_CHECK_CODE_HASH_PREFIX, username)).orElse("0")) >=
                maxFailCount) {
            String checkCode = obtainImageCheckCode(request);
            if (StringUtils.isBlank(checkCode)) {
                log.error("required image check code ,but param not found in request =>{}", username);
                throw new ImageCheckCodeRequiredException();
            }
            //validate image check code
            String _checkCode = this.hashOperations.get(RedisKeySet.IMAGE_CHECK_CODE_HASH_PREFIX, username);
            if (!checkCode.equalsIgnoreCase(_checkCode)) {
                log.error("user=>{} sms authenticate error, image check code mismatch", username);
                throw new ImageCheckCodeMismatchException();
            }
        }

        if (StringUtils.isBlank(smsCode)) {
            logger.error("短信验证码不能为空");
            throw new SmsCodeNotFoundException();
        }

        if (StringUtils.isBlank(username)) {
            logger.error("手机号不能为空");
            throw new MobileNotFoundException();
        }

        smsCode = smsCode.trim();
        username = username.trim();

        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(
                username, smsCode);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        try {
            Authentication authenticate = this.getAuthenticationManager().authenticate(authRequest);
            if (!authenticate.isAuthenticated()) {
                //短信验证码错误 记录次数
                this.hashOperations.increment(RedisKeySet.NEED_IMAGE_CHECK_CODE_HASH_PREFIX, username, 1l);
            } else {
                //认证成功后，重置认证失败次数
                this.hashOperations.delete(RedisKeySet.NEED_IMAGE_CHECK_CODE_HASH_PREFIX, username);
            }
            return authenticate;
        } catch (AuthenticationException e) {
            //短信验证码错误 记录次数
            this.hashOperations.increment(RedisKeySet.NEED_IMAGE_CHECK_CODE_HASH_PREFIX, username, 1l);
            throw e;
        }
    }

    /**
     * Enables subclasses to override the composition of the smsCode, such as by
     * including additional values and a separator.
     *
     * @param request so that request attributes can be retrieved
     * @return the username that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    protected String obtainSmsCode(HttpServletRequest request) {
        return request.getParameter(this.smsCodeParam);
    }

    protected String obtainUserName(HttpServletRequest request) {
        return request.getParameter(this.mobileParam);
    }

    protected String obtainImageCheckCode(HttpServletRequest request) {
        return request.getParameter(this.imageCheckCodeParam);
    }

    /**
     * Provided so that subclasses may configure what is put into the authentication
     * request's details property.
     *
     * @param request     that an authentication request is being created for
     * @param authRequest the authentication request object that should have its details
     *                    set
     */
    protected void setDetails(HttpServletRequest request,
                              SmsAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

}
